/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Diagnostics;
using System.Windows.Data;
using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;

namespace Xceed.Silverlight.Data
{
  internal class GroupCacheEnumerator : CacheEnumerator<DataPath>
  {
    internal GroupCacheEnumerator(
      int cacheSize,
      GroupHelperBase dataHelper,
      IAsyncEnumerator<DataPath> child )
      : base( cacheSize, dataHelper, child )
    {
    }


    private GroupHelperBase GroupHelper
    {
      get
      {
        return ( GroupHelperBase )this.DataHelper;
      }
    }



    internal void ExpansionChanging(
      object sender,
      ExpansionChangedEventArgs e )
    {
      if( e.Action == ExpansionAction.ExpandAll || e.Action == ExpansionAction.CollapseAll )
      {
        this.FlushCache();
      }
      else if( e.Action == ExpansionAction.Expand )
      {
        this.PathExpanded( e.Path );

      }
      else if( e.Action == ExpansionAction.Collapse )
      {
        this.PathCollapsed( e.Path );
      }
    }

    protected virtual void PathExpanded( DataPath path )
    {
      Nullable<int> expandingIndex = null;

      for( int i = 0; i < this.CachedItems.Count; i++ )
      {
        Debug.Assert(
          !path.IsAncestorOf( this.CachedItems[ i ] ),
          "No expanded children of the specified expanding path is supposed to be found" );

        if( path.Equals( this.CachedItems[ i ] ) )
        {
          expandingIndex = i;
          break;
        }
      }

      if( expandingIndex.HasValue )
      {
        this.FlushCache( expandingIndex.Value, false );
      }
    }

    internal void PathCollapsed( DataPath path )
    {
      Nullable<int> collapsingIndex = null;
      Nullable<int> nextGroupIndex = null;

      for( int i = 0; i < this.CachedItems.Count; i++ )
      {
        if( collapsingIndex.HasValue )
        {
          if( !path.IsAncestorOf( this.CachedItems[ i ] ) )
          {
            nextGroupIndex = i;
            break;
          }
        }
        else if( path.IsAncestorOf( this.CachedItems[ i ] ) )
        {
          collapsingIndex = i;
        }
      }

      if( collapsingIndex.HasValue )
      {
        //I use a copy instead of "e.Path" because i don't want the e.Path instance to be "modified" by
        //internal mecanics. This was not the source of a bug, it is just for precaution
        this.CachedItems[ collapsingIndex.Value ] = this.CachedItems[ collapsingIndex.Value ].CreateAncestorPath( path.Depth );
        if( collapsingIndex + 1 < this.CachedItems.Count )
        {
          //If the entire remaining group item is in cache,
          //reuse do an "in cache" collapsing. Keep the non-collapsed items.
          if( nextGroupIndex.HasValue )
          {
            if( ( nextGroupIndex.Value - collapsingIndex ) > 1 )
            {
              int cacheCount = this.CachedItems.Count;
              this.CachedItems.RemoveRange( collapsingIndex.Value + 1, ( nextGroupIndex.Value - collapsingIndex.Value ) - 1 );
            }
          }
          else
          {
            //All the following cache content was actualy part of the collapsed group.
            //Flush that...
            this.FlushCache( collapsingIndex.Value + 1, false );
          }
        }
      }
    }

    protected override void ChildDataItemChanged( ItemChangedInfo itemChangedInfo )
    {
      if( itemChangedInfo == null )
        throw new ArgumentNullException( "itemChangedInfo" );

      int groupCount = this.GroupHelper.GroupHandler.GroupDescriptions.Count;

      //If no grouping exist, just do the base job.
      if( groupCount == 0 )
      {
        base.ChildDataItemChanged( itemChangedInfo );
        return;
      }

      DataPath newCompletePath = ( DataPath )itemChangedInfo.Target;

      if( newCompletePath.Depth <= groupCount )
        throw new DataInternalException();

      int cachedPathIndex = this.GetModifiedItemIndex( newCompletePath );
      DataPath cachedPath = ( cachedPathIndex != -1 ) ? this.CachedItems[ cachedPathIndex ] : null;
      DataPath oldPath = this.GroupHelper.ReCreateOldPath( itemChangedInfo, cachedPath, newCompletePath );

      if( oldPath.Depth >= groupCount
        && DataPath.AreSameItems( oldPath.Path, newCompletePath.Path, groupCount ) )
      {
        //No group property affected.
        base.ChildDataItemChanged( itemChangedInfo );
        return;
      }


      //This Call assume the code that have been handled in GroupEnumerator.
      //Any cases expected to be handled in the GroupEnumerator will throw an exception.
      if( oldPath.Depth >= groupCount )
        throw new DataInternalException();

      int collapsedDepth = this.GroupHelper.GroupHandler.GetCollapsedDepth( newCompletePath );

      //haveOldItem: it was "maybe" part of a collapsed group. can't know for 
      //sure since we do not have the old value. Set is a unknown: "null".
      Nullable<bool> haveOldItem = null;

      //oldItemIndex: We do not have any old value, can only know if it is "before"
      // or "after" the first cache item.
      Nullable<int> oldItemIndex = (cachedPathIndex != -1) 
        ? cachedPathIndex
        : this.GetIncompletePathIndex( oldPath );

      //haveNewItem: We have a new item only if it is not part of a collapsed group.
      Nullable<bool> haveNewItem = ( collapsedDepth == 0 );

      //newItemIndex: At witch place the new item is expected to be inserted ?
      Nullable<int> newItemIndex = ( haveNewItem.Value )
        ? this.GetInsertIndex( newCompletePath )
        : ( Nullable<int> )null;

      //Handle the changes.
      this.HandleItemChangeCore( haveOldItem, oldItemIndex, haveNewItem, newItemIndex, newCompletePath );
    }

    protected override void ChildDataChanged( DataChangedEventArgs args )
    {

      DataPath newItem = ( DataPath )args.NewItem;
      DataPath oldItem = ( DataPath )args.OldItem;

      DataPath newItemToAdd;
      if( args.Action == DataChangedAction.Add
        && this.HandleDataAdded( newItem, out newItemToAdd ) )
      {
        if( newItemToAdd != null )
        {
          this.RaiseDataAdd( newItemToAdd );
        }
      }
      else if( args.Action == DataChangedAction.Replace
        && this.HandleDataAdded( newItem, out newItemToAdd ) )
      {
        this.ChildDataRemoved( oldItem, null );

        if( newItemToAdd != null )
        {
          this.RaiseDataReplaced( oldItem, newItemToAdd );
        }
        else
        {
          this.RaiseDataRemoved( oldItem, null, null );
        }
      }
      else
      {
        //Handle as usual
        base.ChildDataChanged( args );
      }
    }

    protected override void ChildDataRemoved( DataPath oldItem, SourcePosition removePosition )
    {
      if( oldItem == null )
        throw new ArgumentNullException( "oldItem" );

      if( this.CachedItemsCount == 0 )
      {
        base.ChildDataRemoved( oldItem, removePosition );
        return;
      }

      int collapsedDepth = this.GroupHelper.GroupHandler.GetCollapsedDepth( oldItem );

      if( collapsedDepth != 0 && collapsedDepth < oldItem.Depth )
      {
        DataPath collapsedParent = oldItem.CreateAncestorPath( collapsedDepth );

        Nullable<int> removeIndex = this.GetRemoveIndex( collapsedParent );
        if( removeIndex.Value >= 0 && removeIndex.Value < this.CachedItemsCount )
        {

          //We can't know for sure if this collapsed group is still existing. Clear it from
          //the cache to ensure consistency.
          this.FlushCache( removeIndex.Value, false );

          removeIndex = int.MaxValue;
        }

        //This call will update the counts appropriatly.
        this.HandleItemChangeCore( null, removeIndex, false, null, null );
      }
      else
      {
        base.ChildDataRemoved( oldItem, removePosition );
      }

      if( this.GroupHelper.GroupHandler.HasHeadersFooters )
      {

        int levelDiff = -1;
        for(int i=0;i<this.CachedItems.Count;i++)
        {
          DataPath currentItem = this.CachedItems[i];

          for(int j=0;j<currentItem.Depth && j<oldItem.Depth;j++)
          {
            if(!object.Equals(currentItem[j], oldItem[j]))
            {
              if(!(currentItem[j] is HeaderFooterDataItem))
              {
                levelDiff = Math.Max(j, levelDiff);
              }
              break;
            }
          }
        }

        if(levelDiff < oldItem.Depth )
        {
          for(int i=0;i<this.CachedItems.Count;i++)
          {
            DataPath currentItem = this.CachedItems[i];

            for(int j=0;j<currentItem.Depth && j<oldItem.Depth;j++)
            {
              if(!object.Equals(currentItem[j], oldItem[j]))
              {
                if(currentItem[j] is HeaderFooterDataItem)
                {
                  if(levelDiff < j)
                  {
                    this.CachedItems.RemoveAt(i);
                    i--;
                  }
                }
                break;
              }
            }
          }
        }


        this.UpdateCountInformation( this.FirstItemOffset.ToEstimate(), this.TotalItemCount.ToEstimate() );
      }
    }

    private bool HandleDataAdded( DataPath newItem, out DataPath newItemToRaise )
    {
      if( newItem == null )
        throw new ArgumentNullException( "newItem" );

      newItemToRaise = null;

      if( this.CachedItemsCount == 0 )
        return false;

      bool handled = false;

      int collapsedDepth = this.GroupHelper.GroupHandler.GetCollapsedDepth( newItem );

      DataPath addedItem = null;
      bool haveHeadersFooters = this.GroupHelper.GroupHandler.HasHeadersFooters;

      if( collapsedDepth != 0 && collapsedDepth < newItem.Depth )
      {
        DataPath collapsedParent = newItem.CreateAncestorPath( collapsedDepth );

        //Ok it means that there "may" be an Collapsed group to add. 
        //If its is already in cache, there is clearly no item to add...
        if( this.CachedItems.IndexOf( collapsedParent ) == -1 )
        {
          Nullable<bool> haveAnItemToAddForSure = null;
          int insertIndex = this.GetInsertIndex( collapsedParent );

          //If "haveAnItemToAddForSure" is true, this tells us that the item was not in cache but
          //should have been. There is clearly an item to add, wich is the collapsedParent.
          if( insertIndex >= 0 && insertIndex <= this.CachedItemsCount )
          {
            haveAnItemToAddForSure = true;
            addedItem = collapsedParent;
          }
          this.HandleItemChangeCore( false, null, haveAnItemToAddForSure, insertIndex, collapsedParent );
          newItemToRaise = ( haveAnItemToAddForSure.GetValueOrDefault( false ) ) ? collapsedParent : newItem;
        }
        handled = true;
      }
      else if( haveHeadersFooters )
      {
        base.ChildDataAdded( newItem );
        addedItem = newItem;
        newItemToRaise = newItem;
        handled = true;
      }

      if( haveHeadersFooters && addedItem != null)
      {
        this.AddMissingHeadersFootersToCache( false, addedItem );
        this.AddMissingHeadersFootersToCache( true, addedItem );

        this.UpdateCountInformation( this.FirstItemOffset.ToEstimate(), this.TotalItemCount.ToEstimate() );
      }

      return handled;
    }

    private void AddMissingHeadersFootersToCache( bool addHeaders, DataPath addedItem )
    {
      int newItemIndex = this.CachedItems.IndexOf( addedItem );

      if( newItemIndex == -1 )
        return;

      int missingHeadersFootersLevel = -1;

      int cacheLimit = ( addHeaders ) ? 0 : this.CachedItems.Count - 1;
      if( newItemIndex == cacheLimit )
      {
        bool isAtLimit = ( addHeaders ) ? this.IsCacheAtStart : this.IsCacheAtEnd;
        if( !isAtLimit )
        {
          missingHeadersFootersLevel = addedItem.Depth;
        }
      }
      else
      {

        int adjacentIncrement = ( addHeaders ) ? -1 : 1;
        DataPath adjacentItem = this.CachedItems[ newItemIndex + adjacentIncrement ];

        missingHeadersFootersLevel = adjacentItem.Depth - 1;

        Debug.Assert( adjacentItem.Depth >= 0 );

        for( int i = 0; i < adjacentItem.Depth && i < addedItem.Depth; i++ )
        {
          if( !object.Equals( adjacentItem[ i ], addedItem[ i ] ) )
          {
            missingHeadersFootersLevel = i;
            break;
          }
        }
      }

      List<DataPath> headersFooters = new List<DataPath>();
      bool flushRemaining = false;


      for( int i = addedItem.Depth - 2; i >= missingHeadersFootersLevel; i-- )
      {
        if( i == -1 )
          continue;

        DataPath levelPath = addedItem.CreateAncestorPath( i + 1 );

        Debug.Assert( this.GroupHelper.IsGroupPath( levelPath ) );

        IAsyncEnumerator<DataPath> headersFootersEnumerator = ( addHeaders )
          ? this.GroupHelper.GroupHandler.GetHeadersDataPathEnumerator( levelPath )
          : this.GroupHelper.GroupHandler.GetFootersDataPathEnumerator( levelPath );

        if( !headersFootersEnumerator.IsFullySynchronized )
        {
          flushRemaining = true;
          break;
        }
        else
        {
          ISyncEnumerator<DataPath> syncFootersEnumerator = new AsyncToSyncEnumerator<DataPath>( headersFootersEnumerator );
          if( addHeaders )
          {
            headersFooters.InsertRange( 0, syncFootersEnumerator.FetchAll( false ) );
          }
          else
          {
            headersFooters.AddRange( syncFootersEnumerator.FetchAll( false ) );
          }
        }
      }


      if( flushRemaining )
      {
        if( addHeaders )
        {
          if( newItemIndex > 0 )
          {
            this.FlushCache( newItemIndex - 1, true );
          }
          newItemIndex = 0;
        }
        else
        {
          this.FlushCache( newItemIndex + 1, false );
        }
      }

      if( addHeaders )
      {
        this.CachedItems.InsertRange( newItemIndex, headersFooters );
        newItemIndex += headersFooters.Count;
      }
      else
      {
        this.CachedItems.InsertRange( newItemIndex + 1, headersFooters );
      }
    }

    private Nullable<int> GetIncompletePathIndex( DataPath item )
    {
      if( item == null )
        throw new ArgumentNullException( "item" );

      IList<GroupDescription> grouping = this.GroupHelper.GroupHandler.GroupDescriptions;
      DataPath firstCached = this.CachedItems[ 0 ];

      Nullable<int> index = null;
      for( int i = 0; i < grouping.Count; i++ )
      {
        object itemValue;
        object firstCachedValue;

        if( !this.GroupHelper.TryGetMemberValue( item, grouping[ i ], out itemValue )
          || !this.GroupHelper.TryGetMemberValue( firstCached, grouping[ i ], out firstCachedValue ) )
          break;

        index = this.GroupHelper.CompareValues( item, firstCached, grouping[ i ] );

        if( index < 0 )
        {
          index = int.MinValue;
          break;
        }
        else if( index > 0 )
        {
          index = int.MaxValue;
          break;
        }
        else
        {
          index = null;
        }
      }

      return index;
    }

    internal override int GetModifiedItemIndex( DataPath item )
    {
      if( item == null )
        throw new ArgumentNullException( "item" );

      if( ( item.LastChild is HeaderFooterDataItem )
        || ( item.LastChild is GroupDataItem ) )
        throw new ArgumentException( "item is supposed to be a user object" );

      int itemIndex = -1;

      for( int i = 0; i < this.CachedItems.Count; i++ )
      {
        DataPath path = this.CachedItems[ i ];
        if( path.Depth == item.Depth )
        {
          if( object.Equals( path.LastChild, item.LastChild ) )
          {
            bool equal = true;

            for( int j = 0; j < path.Depth - 1; j++ )
            {
              if( !object.Equals( path[ j ], item[ j ] )
                && ( !( path[ j ] is GroupDataItem )
                  || !( item[ j ] is GroupDataItem ) ) )
              {
                equal = false;
                break;
              }
            }

            if( equal )
            {
              itemIndex = i;
              break;
            }
          }
        }
      }

      return itemIndex;
    }

    internal DataPath ProvideCachedPath( DataPath newPath )
    {
      if( newPath == null )
        throw new ArgumentNullException( "newPath" );

      int itemIndex = this.GetModifiedItemIndex( newPath );

      return ( itemIndex != -1 )
        ? this.CachedItems[ itemIndex ]
        : null;
    }
  }
}
